/*
 * CSComFacade.java    0.0.1    23/set/2009
 * 
 * Copyright (c) 2009 mentalsmash.org
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * The use of the Apache License does not indicate that this project is
 * affiliated with the Apache Software Foundation.
 */ 
package org.mentalsmash.tazio.net.cs.server;

import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.net.InetAddress;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

import javax.management.JMException;
import javax.management.MBeanServer;
import javax.management.ObjectName;

import org.mentalsmash.tazio.net.cs.CSSubsystemException;
import org.mentalsmash.tazio.net.cs.server.impl.Acceptor;
import org.mentalsmash.tazio.net.cs.server.impl.AcceptorListener;
import org.mentalsmash.tazio.net.cs.server.impl.BufferPool;
import org.mentalsmash.tazio.net.cs.server.impl.ByteBufferPool;
import org.mentalsmash.tazio.net.cs.server.impl.DeflatingAcceptor;
import org.mentalsmash.tazio.net.cs.server.impl.Dispatcher;
import org.mentalsmash.tazio.net.cs.server.impl.DispatcherListener;
import org.mentalsmash.tazio.net.cs.server.impl.LowLevelDispatcher;
import org.mentalsmash.tazio.net.cs.server.impl.SocketAcceptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The class <tt>CSComFacade</tt> represents an entry point to the LowLevel Client-Server
 * communication subsystem. It manages an internal instance of acceptor and dispatcher,
 * and provides to restart them if needed.
 * <p>
 * Incoming input from clients' connections is managed by threads of a thread pool that is owned
 * by this facade. The nature of this pool can be controlled at facade's construction time. 
 * (See also {@link #CSComFacade(Configuration, Class)} and {@link Configuration} for other details )
 * </p>
 * <p>
 * Threads from the pool will run instances of the 
 * {@link org.mentalsmash.tazio.net.cs.server.ProtocolHandler ProtocolHandler} class
 * upon receiving data. Notice that the same thread may run <code>ProtocolHandler</code>s 
 * for different connections.
 * </p>
 * @see        org.mentalsmash.tazio.net.cs.server.ProtocolHandler ProtocolHandler
 * @see        Configuration
 * @version    0.0.1    23/set/2009    
 * @author     Andrea Reale <andrea.reale@gmail.com>
 */
public class CSComFacade 
implements CSComFacadeMBean, AcceptorListener, DispatcherListener {

    private final static Logger log = LoggerFactory.getLogger(CSComFacade.class);

    public final static String DEFAULT_CONF_PATH = "conf/server.conf";

    //The next two variables are used to check if ports set in the configuration are in 
    //the valid range
    

     private final static int MAX_PORT = 65535;
     private final static int MIN_PORT = 0; // 1024; 


    private final Dispatcher dispatcher;
    private final Acceptor acceptor;

    private final BufferPool bufferPool;
    private final ThreadPoolExecutor threadPool;  // An executor is not necessarily  implemented as a pool of
    // threads, but in this implementation I will use one that it is.

    private volatile boolean running = false;   // true if the whole subsystem is running. i.e. if both the acceptor
    // and the dispatcher are running

    private volatile boolean keepAliveModeOn = false; //If set to true makes this facade try to restart acceptor or dispatcher
    // in the case they stop


    private Class<? extends ProtocolHandler> protocolHandlerClass;

    /*
     * See javadoc for Configuration or for the appropriate getters and setters to
     * see what this fields are for
     */
    private int portNumber = Configuration.DEFAULT_PORT;
    private InetAddress address; //configuread in constructor to localhost, to permit catching exceptions
    private int maxThreadsActive = Configuration.DEFAULT_MAX_THREADS_ACTIVE;
    private boolean fixedThreadPool = Configuration.DEFAULT_FIXED_THREAD_POOL;
    private int maxBuffersIdle = Configuration.DEFAULT_MAX_BUFFERS_IDLE;
    private int maxBuffersActive = Configuration.DEFAULT_MAX_THREADS_ACTIVE;
    private boolean fixedBufferPool = Configuration.DEFAULT_FIXED_BUFFER_POOL;
    private int bufferSize = Configuration.DEFAULT_BUFFER_SIZE;
    private boolean useGzip = Configuration.DEFAULT_USE_GZIP;
    




    /**
     * Creates a new <tt>CSComFacade</tt> using the default configuration file (pointed by {@link #DEFAULT_CONF_PATH}. 
     * or the default conf if the file is not found (For more informations about what are the defaults
     * see {@link Configuration})
     * <p>
     * Initially the <tt>CSComFacade</tt> is not running.
     * </p>
     * @param protocolHandlerClass the class that will be used to handle incoming input from clients' connections
     * @throws CSSubsystemException if some error occurs while creating the threads that will run the subsystem
     * @throws IOException if some IO Exception occurs while opening network communication objects
     * @see Configuration 
     */
    public CSComFacade(Class<? extends ProtocolHandler> protocolHandlerClass) throws CSSubsystemException, IOException {
	this(Configuration.fromFile(DEFAULT_CONF_PATH), protocolHandlerClass);
    }

    /**
     * Creates a new <tt>CSComFacade</tt> using the parameters provided by the specified {@link Configuration} instance.
     * (For more informations see {@link Configuration})
     * <p>
     * Initially the <tt>CSComFacade</tt> is not running.
     * </p>
     * @param conf a {@link Configuration} instance providing initial parameters for the <tt>CSComFacade</tt>
     * @param protocolHandlerClass the class that will be used to handle incoming input from clients' connections
     * @throws CSSubsystemException if some error occurs while creating the threads that will run the subsystem
     * @throws IOException if some IO Exception occurs while opening network communication objects
     * @see Configuration 
     */
    public CSComFacade(Configuration conf, Class<? extends ProtocolHandler> protocolHandlerClass) throws CSSubsystemException, IOException {
	
	parseConfiguration(conf);

	this.protocolHandlerClass = protocolHandlerClass;

	bufferPool = 
	    new ByteBufferPool(maxBuffersActive, maxBuffersIdle, bufferSize, fixedBufferPool);

	//creating the executor according to the fixedThreadPoolParameter
	if(fixedThreadPool){
	    threadPool = (ThreadPoolExecutor) Executors.newFixedThreadPool(maxThreadsActive);
	} else {
	    threadPool = (ThreadPoolExecutor) Executors.newCachedThreadPool();
	}

	dispatcher = new LowLevelDispatcher(threadPool, bufferPool);
	dispatcher.registerListener(this);

	//this.address = InetAddres.getByName("");
	this.address = conf.address == null ? null : InetAddress.getByName(conf.address);
	//FIXME: check whether this works
	acceptor = new SocketAcceptor(dispatcher, protocolHandlerClass, this.address , this.portNumber);
	acceptor.registerListener(this);
	
	if(acceptor instanceof DeflatingAcceptor){
	    log.debug("Setting GZIP option to " + this.useGzip);
	    ((DeflatingAcceptor)acceptor).setDeflating(this.useGzip);
	}
    }

    /**
     * Sets the properties of the Communication subsystem reading them from a configuration instance.
     * This method should be called just by <tt>CSComFacade</tt> constructors, and should therefore
     * be thread-safe.
     */
    private void parseConfiguration(Configuration conf) {
	setPort(conf.portNumber); 	// calling it through the setter because it check if it is a 
	// valid port number

	this.maxThreadsActive = conf.maxThreadsActive;
	this.fixedThreadPool = conf.fixedThreadPool;
	this.maxBuffersIdle = conf.maxBuffersIdle;
	this.maxBuffersActive = conf.maxBuffersActive;
	this.fixedBufferPool = conf.fixedBufferPool;
	this.bufferSize = conf.bufferSize;
	this.useGzip = conf.useGzip;

    }




    /**
     * Starts the <i>CS Communication Subsystem</i>, i.e. it starts the acceptor and the 
     * dispatcher threads, and waits for them to be running before going on.
     * <p>
     * Calling this method on a non-running subsystem makes it start and returns true.
     * On the other hand if the subsystem is already running calling this method does nothing
     * and returns false.
     * </p>
     * @return true if this call actually started the subsystem. False if it was already running
     */
    public synchronized boolean start() {
	if(!running) {
	    log.info("Starting CS Communication Subsystem..");
	    dispatcher.start();
	    acceptor.start();

	    running = true;

	    keepAliveModeOn = true; //tells the subsystem to restart acceptor or dispatcher if the accidentally stop

	    registerMBean();
	    log.info("CS Coommunication Subsystem started");
	    return true;
	} else {
	    return false;
	}
    }

    /**
     * Stops the <i>Client-Server Communication Subsystem</i>, i.e. it stops the dispatcher and
     * acceptor threads.
     */
    public synchronized void stop() {
	if(running) {
	    keepAliveModeOn = false; //tells the subsystem not to try to restart acceptor and dispatcher when they will stop
	    log.info("Stopping CS Communication Subsystem...");
	    acceptor.stop();
	    dispatcher.stop();
	    running = false;
	    log.info("CS Communication Subsystem stopped");
	}
    }

    /**
     * Tries to restart the <i>Client-Server Communication Subsystem</i>.
     * If the system is currently not running it simply starts it.
     */
    public void restart() {
	if(running) {
	    log.info("Restarting CS Communication Subsystem...");
	    stop();
	}
	start();
    }



    /* ***************
     * JMX Interface *
     * ***************/

    /* (non-Javadoc)
     * @see org.mentalsmash.tazio.net.cs.server.jmx.CSComServiceMBean#getNumActiveBuffers()
     */
    @Override
    public int getNumActiveBuffers() {
	return bufferPool.getNumActive();
    }
    
    @Override
    public int getBufferSize() {
	return this.bufferSize;
    }

    /* (non-Javadoc)
     * @see org.mentalsmash.tazio.net.cs.server.jmx.CSComServiceMBean#getNumActiveWorkingThreads()
     */
    @Override
    public int getNumActiveWorkingThreads() {
	return threadPool.getActiveCount();
    }

    /* (non-Javadoc)
     * @see org.mentalsmash.tazio.net.cs.server.jmx.CSComServiceMBean#getNumConnections()
     */
    @Override
    public int getNumConnections() {
	return dispatcher.getNumSources();
    }

    /* (non-Javadoc)
     * @see org.mentalsmash.tazio.net.cs.server.jmx.CSComServiceMBean#getNumIdleBuffers()
     */
    @Override
    public int getNumIdleBuffers() {
	return bufferPool.getNumIdle();
    }

    /* (non-Javadoc)
     * @see org.mentalsmash.tazio.net.cs.server.jmx.CSComServiceMBean#getNumIdleWorkingThreads()
     */
    @Override
    public int getNumIdleWorkingThreads() {
	return threadPool.getPoolSize() - threadPool.getActiveCount();
    }

    /* (non-Javadoc)
     * @see org.mentalsmash.tazio.net.cs.server.jmx.CSComFacadeMBean#getHost()
     */
    @Override
    public String getHost() {
	if(this.address == null) {
	    return "wildcard - this may not work. better to set an actual address";
	}
	return this.address.getHostAddress();
    }


    /**
     * Returns the portNUmber on which the communication subsystem is listening for incoming connections.
     * <p>
     * <strong>SYNCHRONIZATION</strong>: this method is not synchronised with respect to modification to the port. So
     * it may represent a stale values if concurrent threads are changing the port value.
     * Do not use this method if you want to be sure that you are not using invalid values.
     * </p>
     * @return the port number on which the communication subsystem is listening for incoming connections.
     */
    //CONCURRENCY: In spite of the fact the concurrent accesses could modify and read the port number
    //             at the same time, I'm not synchronising this method for two reasons:
    //             a.	The modification on the port number should occur rarely
    //             b.   The getPort() method should be used just for monitoring purposes. So I hope
    //                  it will not be a problem to read stale values.
    @Override
    public int getPort() {
	return this.portNumber;
    }


    /* (non-Javadoc)
     * @see org.mentalsmash.tazio.net.cs.server.jmx.CSComFacadeMBean#setPort(int)
     */

    @Override
    public void setPort(int newPort) {
	
	
	if( (newPort < MIN_PORT) || (newPort > MAX_PORT) ) { //port not in range
	    throw new IllegalArgumentException("Provided port: " + newPort + " is outside " +
		    "the valid port range [" + MIN_PORT + ", " + MIN_PORT + "]");
	}
	
	if(this.isRunning()){
	    throw new UnsupportedOperationException("Changing the port wile the facade is running" +
	    		"is currently not supported");
	}
	
	synchronized (this) {
	    this.portNumber = newPort;
	}
	//Comment becaus the acceptor doesn't actually provide a current mechanism to change its port
	//Implementing such a mechanisms would be more difficult then the benefits of allowing changing
	//the port runtime
//	synchronized (this) {
//	    this.portNumber = newPort;
//
//	    if(acceptor != null && acceptor.isRunning()) {
//		acceptor.stop();
//		//restartAcceptor(); //This is no more needed sinse the keepAlive Mode of the CSComFacade shall do the rest
//		                     //Notice that uncommenting this will cause a glorious deadlock too ^_^
//	    }
//	}
    }

    /**
     * {@inheritDoc}
     */
    public boolean isKeepAliveModeOn() {
	return keepAliveModeOn;
    }

    /**
     * {@inheritDoc}
     */
    public void setKeepAliveModeOn(boolean keepAliveModeOn) {
	this.keepAliveModeOn = keepAliveModeOn;
    }

    /**
     * tries to restart the acceptor thread
     */
    private synchronized void restartAcceptor() {
	if(this.isRunning()) {
	    running = false;
	}

	if(acceptor.isRunning()) {
	    log.warn("Restarting the acceptor, but the acceptor was already running");
	    acceptor.stop();
	}

	acceptor.start();

	if(dispatcher.isDispatching()) { //If the dispatcher is running at this moment tell the world
	    //that the subsystem is running
	    running = true;
	}

    }

    /**
     * tries to restart the dispatcher thread
     */
    private synchronized void restartDispatcher() {
	if(this.isRunning()) {
	    running = false;
	}

	if(acceptor.isRunning()) {
	    log.warn("Restarting the dispatcehr, but it was already running");
	    dispatcher.stop();
	}

	dispatcher.start();

	if(acceptor.isRunning()) { //If the acceptor is running at this moment tell the world
	    //that the subsystem is running
	    running = true;
	}

    }

    /* (non-Javadoc)
     * @see org.mentalsmash.tazio.net.cs.server.jmx.CSComFacadeMBean#isRunning()
     */
    //CONCURRENCY: running is volatile.
    @Override
    public boolean isRunning() {
	return running;
    }

    /* (non-Javadoc)
     * @see org.mentalsmash.tazio.net.cs.server.jmx.getProtocolHandlerClassName()
     */
    @Override
    public String getProtocolHandlerClassName() {
	return protocolHandlerClass.getName();
    }

    /* (non-Javadoc)
     * @see org.mentalsmash.tazio.net.cs.server.jmx.CSComFacadeMBean#getNumRequestsServed()
     */
    @Override
    public int getNumRequestsServed() {
	return dispatcher.getNumRequestsServed();
    }


    /* ************************************************** */
    /* AcceptorListener and DispatcherListener Interfaces */
    /* ************************************************** */

    /* (non-Javadoc)
     * @see org.mentalsmash.tazio.net.cs.server.impl.AcceptorListener#acceptorStarted()
     */
    @Override
    public void acceptorStarted() {
	// Empty method
    }

    /* (non-Javadoc)
     * @see org.mentalsmash.tazio.net.cs.server.impl.AcceptorListener#acceptorStopped(java.lang.Throwable)
     */
    // CONCURRENCY: Even though the running variable is volatile, I need to synchronise on "this",
    //              because some other thread may be already asking a restart
    @Override
    public void acceptorStopped(Throwable cause) {
	log.debug("Acceptor stopped for the following reason: " + cause);

	running = false;
	if(keepAliveModeOn) {
	    synchronized (this) {
		running = false;
		log.debug("Restarting acceptor. It was stopped for a: {}", cause.getClass().getSimpleName() );
		restartAcceptor();

	    }
	}
    }

    /* (non-Javadoc)
     * @see org.mentalsmash.tazio.net.cs.server.impl.DispatcherListener#dispatcherStarted()
     */
    @Override
    public void dispatcherStarted() {
	// Empty method
    }

    /* (non-Javadoc)
     * @see org.mentalsmash.tazio.net.cs.server.impl.DispatcherListener#dispatcherStopped(java.lang.Throwable)
     */
    // CONCURRENCY: Even though the running variable is volatile, I need to synchronise on "this",
    //              because some other thread may be already asking a restart
    @Override
    public void dispatcherStopped(Throwable cause) {
	log.debug("Dispatcher stopped for the following reason: " + cause);


	running = false;
	if(keepAliveModeOn) {
	    synchronized (this) {
		log.debug("Restarting dispatcher...", cause);
		restartDispatcher();

	    }
	}

    }

    /**
     * If the CSComFacade MBean is not yer tregistered on the <i>PlatformMBeanServer</i>
     * registers it
     */
    private void registerMBean() {

	MBeanServer mbs = null;
	ObjectName name = null;

	mbs = ManagementFactory.getPlatformMBeanServer();


	try {
	    name = new ObjectName("org.mentalsmash.tazio.net:type=ServerFacade");
	    if(mbs.isRegistered(name)) {
		log.debug("Mbean {} is already registered.",name);
	    } else {
		mbs.registerMBean(this, name);
	    }
	} catch(JMException e) {
	    log.error("Failed to register CSComFacadeMBean",e);
	} 

    }
    
    /**
     * Kills the acceptor thread with violence.
     * This method should be used for tests only
     */
    protected void violentlyKillAcceptor() {
	acceptor.stop();
    }
    
    @Override
    protected void finalize() {
	stop();
    }

    /* (non-Javadoc)
     * @see org.mentalsmash.tazio.net.cs.server.CSComFacadeMBean#isUsingGzip()
     */
    @Override
    public boolean isUsingGzip() {
	return this.useGzip;
    }



}
